package org.quipu.client;

import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.quipu.client.events.AccountChangedEvent;
import org.quipu.client.events.AccountsOutOfDateEvent;
import org.quipu.client.events.AppEvent;
import org.quipu.client.events.AppEventBus;
import org.quipu.client.events.AppEventListener;
import org.quipu.client.events.BookChangedEvent;
import org.quipu.client.events.BookSelectionChangedEvent;
import org.quipu.client.events.BooksOutOfDateEvent;
import org.quipu.client.events.CategoriesOutOfDateEvent;
import org.quipu.client.events.CategoryChangedEvent;
import org.quipu.client.events.CurrenciesOutOfDateEvent;
import org.quipu.client.events.CurrencyChangedEvent;
import org.quipu.client.events.LoginEvent;
import org.quipu.client.events.TagChangedEvent;
import org.quipu.client.events.TagsOutOfDateEvent;
import org.quipu.dto.AccountDto;
import org.quipu.dto.BookDto;
import org.quipu.dto.CategoryDto;
import org.quipu.dto.CurrencyDto;
import org.quipu.dto.TagDto;
import org.quipu.dto.UserDto;

public class AppCache extends AppEventComponent implements AppEventListener {

    private UserDto user;
    private BookDto book;
    private final Map<Long, BookDto> booksCache;
    private final Map<Long, AccountDto> accountCache;
    private final Map<Long, CurrencyDto> currencyCache;
    private final Map<Long, CategoryDto> inCategoryCache;
    private final Map<Long, CategoryDto> outCategoryCache;
    private final List<CategoryDto> inCategoryTreeCache;
    private final List<CategoryDto> outCategoryTreeCache;
    private final Map<Long, TagDto> tagsCache;

    public AppCache(AppEventBus eventBus) {
        super(eventBus);
        booksCache = new HashMap<Long, BookDto>();
        accountCache = new HashMap<Long, AccountDto>();
        currencyCache = new HashMap<Long, CurrencyDto>();
        inCategoryCache = new HashMap<Long, CategoryDto>();
        outCategoryCache = new HashMap<Long, CategoryDto>();
        inCategoryTreeCache = new LinkedList<CategoryDto>();
        outCategoryTreeCache = new LinkedList<CategoryDto>();
        tagsCache = new HashMap<Long, TagDto>();
    }

    public final UserDto getUser() {
        return user;
    }

    public final void setUser(UserDto user) {
        this.user = user;
    }

    public BookDto getCurrentBook() {
        return book;
    }

    private void setCurrentBook(BookDto book) {
        this.book = book;
    }

    public BookDto getBook(Long id) {
        return booksCache.get(id);
    }

    public Collection<BookDto> getBooks() {
        return booksCache.values();
    }

    public AccountDto getAccount(Long id) {
        return accountCache.get(id);
    }

    public Collection<AccountDto> getAccounts() {
        return accountCache.values();
    }

    public CurrencyDto getCurrency(Long id) {
        return currencyCache.get(id);
    }

    public Collection<CurrencyDto> getCurrencies() {
        return currencyCache.values();
    }

    public CategoryDto getCategory(CategoryDto.Type type, Long id) {
        if (CategoryDto.Type.INCOME.equals(type)) {
            return inCategoryCache.get(id);
        } else {
            return outCategoryCache.get(id);
        }
    }

    public Collection<CategoryDto> getCategories(CategoryDto.Type type) {
        if (CategoryDto.Type.INCOME.equals(type)) {
            return inCategoryCache.values();
        } else {
            return outCategoryCache.values();
        }
    }

    public List<CategoryDto> getCategoryTreeCache(CategoryDto.Type type) {
        if (CategoryDto.Type.INCOME.equals(type)) {
            return inCategoryTreeCache;
        } else {
            return outCategoryTreeCache;
        }
    }

    public TagDto getTag(Long id) {
        return tagsCache.get(id);
    }

    public Collection<TagDto> getTags() {
        return tagsCache.values();
    }

    public void init(UserDto user) {
        setUser(user);
        init();
    }

    public void init() {
        getEventBus().addListener(LoginEvent.class, this);
        getEventBus().addListener(BookSelectionChangedEvent.class, this);
        getEventBus().addListener(BookChangedEvent.class, this);
        getEventBus().addListener(AccountChangedEvent.class, this);
        getEventBus().addListener(CurrencyChangedEvent.class, this);
        getEventBus().addListener(CategoryChangedEvent.class, this);
        getEventBus().addListener(TagChangedEvent.class, this);
    }

    public void handleEvent(AppEvent event) {
        if(event != null) {
            if(event instanceof LoginEvent) {
                LoginEvent loginEvent = (LoginEvent) event;
                setUser(loginEvent.getObject());
                reloadBooks();
            } else if(event instanceof BookSelectionChangedEvent) {
                BookSelectionChangedEvent bookEvent = (BookSelectionChangedEvent) event;
                Long bookId = bookEvent.getObject();
                BookDto selectedBook = getBook(bookId);
                setCurrentBook(selectedBook);
                reloadAccounts();
                reloadCurrencies();
                reloadCategories();
                reloadTags();
            } else if(event instanceof BookChangedEvent) {
                reloadBooks();
            } else if(event instanceof AccountChangedEvent) {
                reloadAccounts();
            } else if(event instanceof CurrencyChangedEvent) {
                reloadCurrencies();
            } else if(event instanceof CategoryChangedEvent) {
                reloadCategories();
            } else if(event instanceof TagChangedEvent) {
                reloadTags();
            }
        }
    }

    private void reloadBooks() {
        ReloadCallback callback = new ReloadCallback(getEventBus(), new BooksOutOfDateEvent(this), 1);
        reloadBooks(callback);
    }

    private void reloadBooks(final ReloadCallback callback) {
        getMainService().getBooks(new AsyncCallback<List<BookDto>>() {

            public void onFailure(Throwable caught) {
                Window.alert(caught.getMessage());
            }

            public void onSuccess(List<BookDto> books) {
                booksCache.clear();
                for(BookDto book: books) {
                    booksCache.put(book.getId(), book);
                }
                callback.call();
            }
        });
    }

    private void reloadAccounts() {
        ReloadCallback callback = new ReloadCallback(getEventBus(), new AccountsOutOfDateEvent(this), 1);
        reloadAccounts(callback);
    }

    private void reloadAccounts(final ReloadCallback callback) {
        getMainService().getBookAccounts(book.getId(), new AsyncCallback<List<AccountDto>>() {

            private final ReloadCallback reloadCallback = callback;

            public void onFailure(Throwable caught) {
                Window.alert(caught.getMessage());
            }

            public void onSuccess(List<AccountDto> accounts) {
                accountCache.clear();
                for(AccountDto account: accounts) {
                    accountCache.put(account.getId(), account);
                }
                reloadCallback.call();
            }
        });
    }

    private void reloadCurrencies() {
        ReloadCallback callback = new ReloadCallback(getEventBus(), new CurrenciesOutOfDateEvent(this), 1);
        reloadCurrencies(callback);
    }

    private void reloadCurrencies(final ReloadCallback callback) {
        getMainService().getBookCurrencies(book.getId(), new AsyncCallback<List<CurrencyDto>>() {

            private final ReloadCallback reloadCallback = callback;

            public void onFailure(Throwable caught) {
                Window.alert(caught.getMessage());
            }

            public void onSuccess(List<CurrencyDto> currencies) {
                currencyCache.clear();
                for(CurrencyDto currency: currencies) {
                    currencyCache.put(currency.getId(), currency);
                }
                reloadCallback.call();
            }
        });
    }

    private void reloadCategories() {
        ReloadCallback callback = new ReloadCallback(getEventBus(), new CategoriesOutOfDateEvent(this), 2);
        reloadCategories(callback, CategoryDto.Type.INCOME);
        reloadCategories(callback, CategoryDto.Type.OUTCOME);
    }

    private void reloadCategories(final ReloadCallback callback, final CategoryDto.Type type) {
        AsyncCallback<List<CategoryDto>> asyncCallback = new AsyncCallback<List<CategoryDto>>() {

            private final ReloadCallback reloadCallback = callback;

            @Override
            public void onFailure(Throwable caught) {
                Window.alert(caught.getMessage());
            }

            @Override
            public void onSuccess(List<CategoryDto> categories) {
                final Map<Long, CategoryDto> categoryCache;
                final List<CategoryDto> categoryTreeCache;
                if(CategoryDto.Type.INCOME.equals(type)) {
                    categoryCache = inCategoryCache;
                    categoryTreeCache = inCategoryTreeCache;
                } else {
                    categoryCache = outCategoryCache;
                    categoryTreeCache = outCategoryTreeCache;
                }
                categoryTreeCache.clear();
                categoryTreeCache.addAll(categories);
                populateCache(categoryCache, categories);
                reloadCallback.call();
            }

            private void populateCache(Map<Long, CategoryDto> categoryCache, List<CategoryDto> categories) {
                categoryCache.clear();
                for(CategoryDto rootCategory: categories) {
                    populateParent(categoryCache, rootCategory);
                }
            }

            private void populateParent(Map<Long, CategoryDto> categoryCache, CategoryDto parent) {
                categoryCache.put(parent.getId(), parent);
                for(CategoryDto child: parent.getChilds()) {
                    populateParent(categoryCache, child);
                }
            }
        };
        if(CategoryDto.Type.INCOME.equals(type)) {
            getMainService().getBookInCategories(book.getId(), asyncCallback);
        } else {
            getMainService().getBookOutCategories(book.getId(), asyncCallback);
        }
    }

    private void reloadTags() {
        ReloadCallback callback = new ReloadCallback(getEventBus(), new TagsOutOfDateEvent(this), 1);
        reloadTags(callback);
    }

    private void reloadTags(final ReloadCallback callback) {
        getMainService().getBookTags(book.getId(), new AsyncCallback<List<TagDto>>() {

            public void onFailure(Throwable caught) {
                Window.alert(caught.getMessage());
            }

            public void onSuccess(List<TagDto> tags) {
                tagsCache.clear();
                for(TagDto tag: tags) {
                    tagsCache.put(tag.getId(), tag);
                }
                callback.call();
            }
        });
    }
}
